{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Deploying a MedNIST Classifier App with MONAI Deploy App SDK (Prebuilt Model)\n",
    "\n",
    "This tutorial demos the process of packaging up a trained model using MONAI Deploy App SDK into an deployable inference application which can be run as a local program, as well as an MONAI Application Package (MAP) for containerized workflow execution."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Clone the github project (the latest version of the main branch only)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Cloning into 'source'...\n",
      "remote: Enumerating objects: 314, done.\u001b[K\n",
      "remote: Counting objects: 100% (314/314), done.\u001b[K\n",
      "remote: Compressing objects: 100% (254/254), done.\u001b[K\n",
      "remote: Total 314 (delta 71), reused 184 (delta 36), pack-reused 0 (from 0)\u001b[K\n",
      "Receiving objects: 100% (314/314), 1.47 MiB | 3.95 MiB/s, done.\n",
      "Resolving deltas: 100% (71/71), done.\n"
     ]
    }
   ],
   "source": [
    "!rm -rf source \\\n",
    " && git clone --branch main --depth 1 https://github.com/Project-MONAI/monai-deploy-app-sdk.git source \\\n",
    " && rm -rf source/.git"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "app.yaml  mednist_classifier_monaideploy.py  requirements.txt\n"
     ]
    }
   ],
   "source": [
    "!ls source/examples/apps/mednist_classifier_monaideploy/"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Install monai-deploy-app-sdk package"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Requirement already satisfied: monai-deploy-app-sdk in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (0.5.1+37.g96f7e31.dirty)\n",
      "Requirement already satisfied: numpy>=1.21.6 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from monai-deploy-app-sdk) (1.26.4)\n",
      "Requirement already satisfied: holoscan~=3.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from monai-deploy-app-sdk) (3.1.0)\n",
      "Requirement already satisfied: holoscan-cli~=3.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from monai-deploy-app-sdk) (3.1.0)\n",
      "Requirement already satisfied: colorama>=0.4.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from monai-deploy-app-sdk) (0.4.6)\n",
      "Requirement already satisfied: tritonclient>=2.53.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (2.56.0)\n",
      "Requirement already satisfied: typeguard>=3.0.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from monai-deploy-app-sdk) (4.4.2)\n",
      "Requirement already satisfied: pip>22.0.2 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from holoscan~=3.0->monai-deploy-app-sdk) (25.0.1)\n",
      "Requirement already satisfied: cupy-cuda12x<14.0,>=12.2 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from holoscan~=3.0->monai-deploy-app-sdk) (13.4.1)\n",
      "Requirement already satisfied: cloudpickle<4.0,>=3.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from holoscan~=3.0->monai-deploy-app-sdk) (3.1.1)\n",
      "Requirement already satisfied: wheel-axle-runtime<1.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from holoscan~=3.0->monai-deploy-app-sdk) (0.0.6)\n",
      "Requirement already satisfied: Jinja2<4.0.0,>=3.1.5 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from holoscan-cli~=3.0->monai-deploy-app-sdk) (3.1.6)\n",
      "Requirement already satisfied: packaging<24.0,>=23.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from holoscan-cli~=3.0->monai-deploy-app-sdk) (23.2)\n",
      "Requirement already satisfied: psutil<7.0.0,>=6.0.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from holoscan-cli~=3.0->monai-deploy-app-sdk) (6.1.1)\n",
      "Requirement already satisfied: python-on-whales<0.61.0,>=0.60.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from holoscan-cli~=3.0->monai-deploy-app-sdk) (0.60.1)\n",
      "Requirement already satisfied: pyyaml<7.0,>=6.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from holoscan-cli~=3.0->monai-deploy-app-sdk) (6.0.2)\n",
      "Requirement already satisfied: requests<3.0.0,>=2.31.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from holoscan-cli~=3.0->monai-deploy-app-sdk) (2.32.3)\n",
      "Requirement already satisfied: python-rapidjson>=0.9.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from tritonclient>=2.53.0->tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (1.20)\n",
      "Requirement already satisfied: urllib3>=2.0.7 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from tritonclient>=2.53.0->tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (2.4.0)\n",
      "Requirement already satisfied: aiohttp<4.0.0,>=3.8.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (3.11.18)\n",
      "Requirement already satisfied: cuda-python in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (12.8.0)\n",
      "Requirement already satisfied: geventhttpclient>=2.3.3 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (2.3.3)\n",
      "Requirement already satisfied: grpcio<1.68,>=1.63.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (1.67.1)\n",
      "Requirement already satisfied: protobuf<6.0dev,>=5.26.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (5.29.4)\n",
      "Requirement already satisfied: typing_extensions>=4.10.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from typeguard>=3.0.0->monai-deploy-app-sdk) (4.13.2)\n",
      "Requirement already satisfied: aiohappyeyeballs>=2.3.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from aiohttp<4.0.0,>=3.8.1->tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (2.6.1)\n",
      "Requirement already satisfied: aiosignal>=1.1.2 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from aiohttp<4.0.0,>=3.8.1->tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (1.3.2)\n",
      "Requirement already satisfied: async-timeout<6.0,>=4.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from aiohttp<4.0.0,>=3.8.1->tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (5.0.1)\n",
      "Requirement already satisfied: attrs>=17.3.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from aiohttp<4.0.0,>=3.8.1->tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (25.3.0)\n",
      "Requirement already satisfied: frozenlist>=1.1.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from aiohttp<4.0.0,>=3.8.1->tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (1.6.0)\n",
      "Requirement already satisfied: multidict<7.0,>=4.5 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from aiohttp<4.0.0,>=3.8.1->tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (6.4.3)\n",
      "Requirement already satisfied: propcache>=0.2.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from aiohttp<4.0.0,>=3.8.1->tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (0.3.1)\n",
      "Requirement already satisfied: yarl<2.0,>=1.17.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from aiohttp<4.0.0,>=3.8.1->tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (1.20.0)\n",
      "Requirement already satisfied: fastrlock>=0.5 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from cupy-cuda12x<14.0,>=12.2->holoscan~=3.0->monai-deploy-app-sdk) (0.8.3)\n",
      "Requirement already satisfied: gevent in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from geventhttpclient>=2.3.3->tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (25.4.1)\n",
      "Requirement already satisfied: certifi in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from geventhttpclient>=2.3.3->tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (2025.1.31)\n",
      "Requirement already satisfied: brotli in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from geventhttpclient>=2.3.3->tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (1.1.0)\n",
      "Requirement already satisfied: MarkupSafe>=2.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from Jinja2<4.0.0,>=3.1.5->holoscan-cli~=3.0->monai-deploy-app-sdk) (3.0.2)\n",
      "Requirement already satisfied: pydantic<2,>=1.5 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from python-on-whales<0.61.0,>=0.60.1->holoscan-cli~=3.0->monai-deploy-app-sdk) (1.10.21)\n",
      "Requirement already satisfied: tqdm in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from python-on-whales<0.61.0,>=0.60.1->holoscan-cli~=3.0->monai-deploy-app-sdk) (4.67.1)\n",
      "Requirement already satisfied: typer>=0.4.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from python-on-whales<0.61.0,>=0.60.1->holoscan-cli~=3.0->monai-deploy-app-sdk) (0.15.2)\n",
      "Requirement already satisfied: charset-normalizer<4,>=2 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from requests<3.0.0,>=2.31.0->holoscan-cli~=3.0->monai-deploy-app-sdk) (3.4.1)\n",
      "Requirement already satisfied: idna<4,>=2.5 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from requests<3.0.0,>=2.31.0->holoscan-cli~=3.0->monai-deploy-app-sdk) (3.10)\n",
      "Requirement already satisfied: filelock in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from wheel-axle-runtime<1.0->holoscan~=3.0->monai-deploy-app-sdk) (3.18.0)\n",
      "Requirement already satisfied: cuda-bindings~=12.8.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from cuda-python->tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (12.8.0)\n",
      "Requirement already satisfied: click>=8.0.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from typer>=0.4.1->python-on-whales<0.61.0,>=0.60.1->holoscan-cli~=3.0->monai-deploy-app-sdk) (8.1.8)\n",
      "Requirement already satisfied: shellingham>=1.3.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from typer>=0.4.1->python-on-whales<0.61.0,>=0.60.1->holoscan-cli~=3.0->monai-deploy-app-sdk) (1.5.4)\n",
      "Requirement already satisfied: rich>=10.11.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from typer>=0.4.1->python-on-whales<0.61.0,>=0.60.1->holoscan-cli~=3.0->monai-deploy-app-sdk) (14.0.0)\n",
      "Requirement already satisfied: greenlet>=3.2.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from gevent->geventhttpclient>=2.3.3->tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (3.2.0)\n",
      "Requirement already satisfied: zope.event in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from gevent->geventhttpclient>=2.3.3->tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (5.0)\n",
      "Requirement already satisfied: zope.interface in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from gevent->geventhttpclient>=2.3.3->tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (7.2)\n",
      "Requirement already satisfied: markdown-it-py>=2.2.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from rich>=10.11.0->typer>=0.4.1->python-on-whales<0.61.0,>=0.60.1->holoscan-cli~=3.0->monai-deploy-app-sdk) (3.0.0)\n",
      "Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from rich>=10.11.0->typer>=0.4.1->python-on-whales<0.61.0,>=0.60.1->holoscan-cli~=3.0->monai-deploy-app-sdk) (2.19.1)\n",
      "Requirement already satisfied: setuptools in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from zope.event->gevent->geventhttpclient>=2.3.3->tritonclient[all]>=2.53.0->monai-deploy-app-sdk) (79.0.0)\n",
      "Requirement already satisfied: mdurl~=0.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from markdown-it-py>=2.2.0->rich>=10.11.0->typer>=0.4.1->python-on-whales<0.61.0,>=0.60.1->holoscan-cli~=3.0->monai-deploy-app-sdk) (0.1.2)\n"
     ]
    }
   ],
   "source": [
    "!pip install monai-deploy-app-sdk"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Install necessary packages for the app"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Requirement already satisfied: monai in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (1.4.0)\n",
      "Requirement already satisfied: Pillow in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (11.2.1)\n",
      "Requirement already satisfied: numpy<2.0,>=1.24 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from monai) (1.26.4)\n",
      "Requirement already satisfied: torch>=1.9 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from monai) (2.6.0)\n",
      "Requirement already satisfied: filelock in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (3.18.0)\n",
      "Requirement already satisfied: typing-extensions>=4.10.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (4.13.2)\n",
      "Requirement already satisfied: networkx in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (3.4.2)\n",
      "Requirement already satisfied: jinja2 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (3.1.6)\n",
      "Requirement already satisfied: fsspec in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (2025.3.2)\n",
      "Requirement already satisfied: nvidia-cuda-nvrtc-cu12==12.4.127 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (12.4.127)\n",
      "Requirement already satisfied: nvidia-cuda-runtime-cu12==12.4.127 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (12.4.127)\n",
      "Requirement already satisfied: nvidia-cuda-cupti-cu12==12.4.127 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (12.4.127)\n",
      "Requirement already satisfied: nvidia-cudnn-cu12==9.1.0.70 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (9.1.0.70)\n",
      "Requirement already satisfied: nvidia-cublas-cu12==12.4.5.8 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (12.4.5.8)\n",
      "Requirement already satisfied: nvidia-cufft-cu12==11.2.1.3 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (11.2.1.3)\n",
      "Requirement already satisfied: nvidia-curand-cu12==10.3.5.147 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (10.3.5.147)\n",
      "Requirement already satisfied: nvidia-cusolver-cu12==11.6.1.9 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (11.6.1.9)\n",
      "Requirement already satisfied: nvidia-cusparse-cu12==12.3.1.170 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (12.3.1.170)\n",
      "Requirement already satisfied: nvidia-cusparselt-cu12==0.6.2 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (0.6.2)\n",
      "Requirement already satisfied: nvidia-nccl-cu12==2.21.5 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (2.21.5)\n",
      "Requirement already satisfied: nvidia-nvtx-cu12==12.4.127 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (12.4.127)\n",
      "Requirement already satisfied: nvidia-nvjitlink-cu12==12.4.127 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (12.4.127)\n",
      "Requirement already satisfied: triton==3.2.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (3.2.0)\n",
      "Requirement already satisfied: sympy==1.13.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from torch>=1.9->monai) (1.13.1)\n",
      "Requirement already satisfied: mpmath<1.4,>=1.1.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from sympy==1.13.1->torch>=1.9->monai) (1.3.0)\n",
      "Requirement already satisfied: MarkupSafe>=2.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages (from jinja2->torch>=1.9->monai) (3.0.2)\n"
     ]
    }
   ],
   "source": [
    "!pip install monai Pillow # for MONAI transforms and Pillow\n",
    "!python -c \"import pydicom\" || pip install -q \"pydicom>=1.4.2\"\n",
    "!python -c \"import highdicom\" || pip install -q \"highdicom>=0.18.2\" # for the use of DICOM Writer operators"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Download/Extract mednist_classifier_data.zip from Google Drive\n",
    "\n",
    "**_Note:_** Data files are now access controlled. Please first request permission to access the [shared folder on Google Drive](https://drive.google.com/drive/folders/1EONJsrwbGsS30td0hs8zl4WKjihew1Z3?usp=sharing), then download the zip file, `mednist_classifier_data.zip` found in the `medmist_classifier_app` folder, to the same folder as this notebook example."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "data_dir = os.path.join(os.path.curdir, \"mednist_classifier_data.zip\")\n",
    "if not os.path.exists(data_dir):\n",
    "    # Download mednist_classifier_data.zip\n",
    "    !pip install gdown\n",
    "    !gdown \"https://drive.google.com/uc?id=1IoEJZFFixcNtPPKeKZfD_xSJSFQCbawl\" # Redundant if already manually downloaded the file to avoid permission issue."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Archive:  mednist_classifier_data.zip\n",
      " extracting: classifier.zip          \n",
      " extracting: input/AbdomenCT_007000.jpeg  \n",
      "classifier.zip\n"
     ]
    }
   ],
   "source": [
    "# Unzip the downloaded mednist_classifier_data.zip from the web browser or using gdown, to the notebook/turotials folder, and set up folders\n",
    "input_folder = \"input\"\n",
    "output_folder = \"output\"\n",
    "models_folder = \"models\"\n",
    "!rm -rf {input_folder}\n",
    "!unzip -o \"mednist_classifier_data.zip\"\n",
    "\n",
    "# Need to copy the model file to its own clean subfolder for packaging, to workaround an issue in the Packager\n",
    "models_folder = \"models\"\n",
    "!rm -rf {models_folder} && mkdir -p {models_folder}/model && cp classifier.zip {models_folder}/model && ls {models_folder}/model"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Set up environment variables\n",
    "The application uses well-known environment variables for the input/output data path, working dir, as well as AI model file path if applicable. Defaults are used if these environment variable are absent.\n",
    "\n",
    "Set the environment variables corresponding to the extracted data path."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "env: HOLOSCAN_INPUT_PATH=input\n",
      "env: HOLOSCAN_OUTPUT_PATH=output\n",
      "env: HOLOSCAN_MODEL_PATH=models\n"
     ]
    }
   ],
   "source": [
    "%env HOLOSCAN_INPUT_PATH {input_folder}\n",
    "%env HOLOSCAN_OUTPUT_PATH {output_folder}\n",
    "%env HOLOSCAN_MODEL_PATH {models_folder}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Package app (creating MAP container image)\n",
    "\n",
    "Now we can use the CLI package command to build the MONAI Application Package (MAP) container image based on a supported base image\n",
    "\n",
    "Use `-l DEBUG` option to see progress.\n",
    "\n",
    ":::{note}\n",
    "This assumes that <a href=\"https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html\">NVIDIA Container Toolkit or nvidia docker</a> is installed on the local machine.\n",
    ":::"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "usage: monai-deploy package [-h] [-l {DEBUG,INFO,WARN,ERROR,CRITICAL}]\n",
      "                            --config CONFIG [--docs DOCS] [--models MODELS]\n",
      "                            --platform PLATFORM [--add ADDITIONAL_LIBS]\n",
      "                            [--timeout TIMEOUT] [--version VERSION]\n",
      "                            [--base-image BASE_IMAGE]\n",
      "                            [--build-image BUILD_IMAGE]\n",
      "                            [--includes [{debug,holoviz,torch,onnx} ...]]\n",
      "                            [--build-cache BUILD_CACHE]\n",
      "                            [--cmake-args CMAKE_ARGS]\n",
      "                            [--holoscan-sdk-file HOLOSCAN_SDK_FILE]\n",
      "                            [--monai-deploy-sdk-file MONAI_DEPLOY_SDK_FILE]\n",
      "                            [--no-cache] [--sdk SDK] [--source SOURCE]\n",
      "                            [--sdk-version SDK_VERSION] [--output OUTPUT]\n",
      "                            --tag TAG [--username USERNAME] [--uid UID]\n",
      "                            [--gid GID]\n",
      "                            application\n",
      "monai-deploy package: error: argument --platform: x64-workstation is not a valid option for --platforms.\n"
     ]
    }
   ],
   "source": [
    "tag_prefix = \"mednist_app\"\n",
    "\n",
    "!monai-deploy package \"source/examples/apps/mednist_classifier_monaideploy/mednist_classifier_monaideploy.py\" -m {models_folder} -c \"source/examples/apps/mednist_classifier_monaideploy/app.yaml\" -t {tag_prefix}:1.0 --platform x64-workstation -l DEBUG"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can see that the MAP Docker image is created"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "!docker image ls | grep {tag_prefix}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can choose to display and inspect the MAP manifests by running the container with the `show` command.\n",
    "Furthermore, we can also extract the manifests and other contents in the MAP by using the `extract` command while mapping specific folder to the host's (we know that our MAP is compliant and supports these commands).\n",
    "\n",
    ":::{note}\n",
    "The host folder for storing the extracted content must first be created by the user, and if it has been created by Docker on running the container, the folder needs to be deleted and re-created.\n",
    ":::"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Display manifests and extract MAP contents to the host folder, ./export\n",
      "Unable to find image 'mednist_app-x64-workstation-dgpu-linux-amd64:1.0' locally\n",
      "docker: Error response from daemon: pull access denied for mednist_app-x64-workstation-dgpu-linux-amd64, repository does not exist or may require 'docker login': denied: requested access to the resource is denied\n",
      "\n",
      "Run 'docker run --help' for more information\n",
      "Unable to find image 'mednist_app-x64-workstation-dgpu-linux-amd64:1.0' locally\n",
      "docker: Error response from daemon: pull access denied for mednist_app-x64-workstation-dgpu-linux-amd64, repository does not exist or may require 'docker login': denied: requested access to the resource is denied\n",
      "\n",
      "Run 'docker run --help' for more information\n"
     ]
    }
   ],
   "source": [
    "!echo \"Display manifests and extract MAP contents to the host folder, ./export\"\n",
    "!docker run --rm {tag_prefix}-x64-workstation-dgpu-linux-amd64:1.0 show\n",
    "!rm -rf `pwd`/export && mkdir -p `pwd`/export\n",
    "!docker run --rm -v `pwd`/export/:/var/run/holoscan/export/ {tag_prefix}-x64-workstation-dgpu-linux-amd64:1.0 extract\n",
    "!ls `pwd`/export"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Executing packaged app locally\n",
    "\n",
    "The packaged app can be run locally through [MONAI Application Runner](/developing_with_sdk/executing_packaged_app_locally)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[2025-04-22 10:01:00,178] [INFO] (runner) - Checking dependencies...\n",
      "[2025-04-22 10:01:00,178] [INFO] (runner) - --> Verifying if \"docker\" is installed...\n",
      "\n",
      "[2025-04-22 10:01:00,179] [INFO] (runner) - --> Verifying if \"docker-buildx\" is installed...\n",
      "\n",
      "[2025-04-22 10:01:00,179] [INFO] (runner) - --> Verifying if \"mednist_app-x64-workstation-dgpu-linux-amd64:1.0\" is available...\n",
      "\n",
      "[2025-04-22 10:01:00,206] [INFO] (common) - Attempting to pull image mednist_app-x64-workstation-dgpu-linux-amd64:1.0..\n",
      "Error response from daemon: pull access denied for mednist_app-x64-workstation-dgpu-linux-amd64, repository does not exist or may require 'docker login': denied: requested access to the resource is denied\n",
      "[2025-04-22 10:01:01,166] [ERROR] (common) - The docker command executed was `/usr/bin/docker image pull mednist_app-x64-workstation-dgpu-linux-amd64:1.0`.\n",
      "It returned with code 1\n",
      "The content of stdout can be found above the stacktrace (it wasn't captured).\n",
      "The content of stderr can be found above the stacktrace (it wasn't captured).\n",
      "[2025-04-22 10:01:01,166] [ERROR] (runner) - Unable to fetch required image.\n",
      "[2025-04-22 10:01:01,167] [ERROR] (runner) - Execution Aborted\n"
     ]
    }
   ],
   "source": [
    "# Clear the output folder and run the MAP. The input is expected to be a folder.\n",
    "!rm -rf {ouput_folder}\n",
    "!monai-deploy run -i $HOLOSCAN_INPUT_PATH -o $HOLOSCAN_OUTPUT_PATH mednist_app-x64-workstation-dgpu-linux-amd64:1.0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "cat: output/output.json: No such file or directory\n"
     ]
    }
   ],
   "source": [
    "!cat {output_folder}/output.json"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Implementing and Packaging Application with MONAI Deploy App SDK\n",
    "\n",
    "In the following sections we will discuss the details of buildng the application that was packaged and run above.\n",
    "\n",
    "Based on the Torchscript model(`classifier.zip`), we will implement an application that process an input Jpeg image and write the prediction(classification) result as JSON file(`output.json`).\n",
    "\n",
    "In our inference application, we will define two operators:\n",
    "\n",
    "1. `LoadPILOperator` - Load a JPEG image from the input path and pass the loaded image object to the next operator.\n",
    "    - **Input**: a file path (`Path`)\n",
    "    - **Output**: an image object in memory ([`Image`](/modules/_autosummary/monai.deploy.core.domain.Image))\n",
    "2. `MedNISTClassifierOperator` - Pre-transform the given image by using MONAI's `Compose` class, feed to the Torchscript model (`classifier.zip`), and write the prediction into JSON file(`output.json`)\n",
    "    - Pre-transforms consist of three transforms -- `EnsureChannelFirst`, `ScaleIntensity`, and `EnsureType`.\n",
    "    - **Input**: an image object in memory ([`Image`](/modules/_autosummary/monai.deploy.core.domain.Image))\n",
    "    - **Output**: a folder path that the prediction result(`output.json`) would be written (`Path`)\n",
    "\n",
    "The workflow of the application would look like this.\n",
    "\n",
    "<img src=\"https://user-images.githubusercontent.com/1928522/133868503-46671f0a-7741-4f9d-aefa-83e95e9a5f84.png\" alt=\"Workflow\" style=\"width: 600px;margin-left:auto;margin-right:auto;\"/>\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Setup imports\n",
    "\n",
    "Let's import necessary classes/decorators and define `MEDNIST_CLASSES`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "import logging\n",
    "import os\n",
    "from pathlib import Path\n",
    "from typing import Optional\n",
    "\n",
    "import torch\n",
    "\n",
    "from monai.deploy.conditions import CountCondition\n",
    "from monai.deploy.core import AppContext, Application, ConditionType, Fragment, Image, Operator, OperatorSpec\n",
    "from monai.deploy.operators.dicom_text_sr_writer_operator import DICOMTextSRWriterOperator, EquipmentInfo, ModelInfo\n",
    "from monai.transforms import EnsureChannelFirst, Compose, EnsureType, ScaleIntensity\n",
    "\n",
    "MEDNIST_CLASSES = [\"AbdomenCT\", \"BreastMRI\", \"CXR\", \"ChestCT\", \"Hand\", \"HeadCT\"]"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Creating Operator classes\n",
    "\n",
    "#### LoadPILOperator"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "class LoadPILOperator(Operator):\n",
    "    \"\"\"Load image from the given input (DataPath) and set numpy array to the output (Image).\"\"\"\n",
    "\n",
    "    DEFAULT_INPUT_FOLDER = Path.cwd() / \"input\"\n",
    "    DEFAULT_OUTPUT_NAME = \"image\"\n",
    "\n",
    "    # For now, need to have the input folder as an instance attribute, set on init.\n",
    "    # If dynamically changing the input folder, per compute, then use a (optional) input port to convey the\n",
    "    # value of the input folder, which is then emitted by a upstream operator.\n",
    "    def __init__(\n",
    "        self,\n",
    "        fragment: Fragment,\n",
    "        *args,\n",
    "        input_folder: Path = DEFAULT_INPUT_FOLDER,\n",
    "        output_name: str = DEFAULT_OUTPUT_NAME,\n",
    "        **kwargs,\n",
    "    ):\n",
    "        \"\"\"Creates an loader object with the input folder and the output port name overrides as needed.\n",
    "\n",
    "        Args:\n",
    "            fragment (Fragment): An instance of the Application class which is derived from Fragment.\n",
    "            input_folder (Path): Folder from which to load input file(s).\n",
    "                                 Defaults to `input` in the current working directory.\n",
    "            output_name (str): Name of the output port, which is an image object. Defaults to `image`.\n",
    "        \"\"\"\n",
    "\n",
    "        self._logger = logging.getLogger(\"{}.{}\".format(__name__, type(self).__name__))\n",
    "        self.input_path = input_folder\n",
    "        self.index = 0\n",
    "        self.output_name_image = (\n",
    "            output_name.strip() if output_name and len(output_name.strip()) > 0 else LoadPILOperator.DEFAULT_OUTPUT_NAME\n",
    "        )\n",
    "\n",
    "        super().__init__(fragment, *args, **kwargs)\n",
    "\n",
    "    def setup(self, spec: OperatorSpec):\n",
    "        \"\"\"Set up the named input and output port(s)\"\"\"\n",
    "        spec.output(self.output_name_image)\n",
    "\n",
    "    def compute(self, op_input, op_output, context):\n",
    "        import numpy as np\n",
    "        from PIL import Image as PILImage\n",
    "\n",
    "        # Input path is stored in the object attribute, but could change to use a named port if need be.\n",
    "        input_path = self.input_path\n",
    "        if input_path.is_dir():\n",
    "            input_path = next(self.input_path.glob(\"*.*\"))  # take the first file\n",
    "\n",
    "        image = PILImage.open(input_path)\n",
    "        image = image.convert(\"L\")  # convert to greyscale image\n",
    "        image_arr = np.asarray(image)\n",
    "\n",
    "        output_image = Image(image_arr)  # create Image domain object with a numpy array\n",
    "        op_output.emit(output_image, self.output_name_image)  # cannot omit the name even if single output.\n",
    "\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### MedNISTClassifierOperator"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MedNISTClassifierOperator(Operator):\n",
    "    \"\"\"Classifies the given image and returns the class name.\n",
    "\n",
    "    Named inputs:\n",
    "        image: Image object for which to generate the classification.\n",
    "        output_folder: Optional, the path to save the results JSON file, overridingthe the one set on __init__\n",
    "\n",
    "    Named output:\n",
    "        result_text: The classification results in text.\n",
    "    \"\"\"\n",
    "\n",
    "    DEFAULT_OUTPUT_FOLDER = Path.cwd() / \"classification_results\"\n",
    "    # For testing the app directly, the model should be at the following path.\n",
    "    MODEL_LOCAL_PATH = Path(os.environ.get(\"HOLOSCAN_MODEL_PATH\", Path.cwd() / \"model/model.ts\"))\n",
    "\n",
    "    def __init__(\n",
    "        self,\n",
    "        fragment: Fragment,\n",
    "        *args,\n",
    "        app_context: AppContext,\n",
    "        model_name: Optional[str] = \"\",\n",
    "        model_path: Path = MODEL_LOCAL_PATH,\n",
    "        output_folder: Path = DEFAULT_OUTPUT_FOLDER,\n",
    "        **kwargs,\n",
    "    ):\n",
    "        \"\"\"Creates an instance with the reference back to the containing application/fragment.\n",
    "\n",
    "        fragment (Fragment): An instance of the Application class which is derived from Fragment.\n",
    "        model_name (str, optional): Name of the model. Default to \"\" for single model app.\n",
    "        model_path (Path): Path to the model file. Defaults to model/models.ts of current working dir.\n",
    "        output_folder (Path, optional): output folder for saving the classification results JSON file.\n",
    "        \"\"\"\n",
    "\n",
    "        # the names used for the model inference input and output\n",
    "        self._input_dataset_key = \"image\"\n",
    "        self._pred_dataset_key = \"pred\"\n",
    "\n",
    "        # The names used for the operator input and output\n",
    "        self.input_name_image = \"image\"\n",
    "        self.output_name_result = \"result_text\"\n",
    "\n",
    "        # The name of the optional input port for passing data to override the output folder path.\n",
    "        self.input_name_output_folder = \"output_folder\"\n",
    "\n",
    "        # The output folder set on the object can be overridden at each compute by data in the optional named input\n",
    "        self.output_folder = output_folder\n",
    "\n",
    "        # Need the name when there are multiple models loaded\n",
    "        self._model_name = model_name.strip() if isinstance(model_name, str) else \"\"\n",
    "        # Need the path to load the models when they are not loaded in the execution context\n",
    "        self.model_path = model_path\n",
    "        self.app_context = app_context\n",
    "        self.model = self._get_model(self.app_context, self.model_path, self._model_name)\n",
    "\n",
    "        # This needs to be at the end of the constructor.\n",
    "        super().__init__(fragment, *args, **kwargs)\n",
    "\n",
    "    def _get_model(self, app_context: AppContext, model_path: Path, model_name: str):\n",
    "        \"\"\"Load the model with the given name from context or model path\n",
    "\n",
    "        Args:\n",
    "            app_context (AppContext): The application context object holding the model(s)\n",
    "            model_path (Path): The path to the model file, as a backup to load model directly\n",
    "            model_name (str): The name of the model, when multiples are loaded in the context\n",
    "        \"\"\"\n",
    "\n",
    "        if app_context.models:\n",
    "            # `app_context.models.get(model_name)` returns a model instance if exists.\n",
    "            # If model_name is not specified and only one model exists, it returns that model.\n",
    "            model = app_context.models.get(model_name)\n",
    "        else:\n",
    "            model = torch.jit.load(\n",
    "                MedNISTClassifierOperator.MODEL_LOCAL_PATH,\n",
    "                map_location=torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\"),\n",
    "            )\n",
    "\n",
    "        return model\n",
    "\n",
    "    def setup(self, spec: OperatorSpec):\n",
    "        \"\"\"Set up the operator named input and named output, both are in-memory objects.\"\"\"\n",
    "\n",
    "        spec.input(self.input_name_image)\n",
    "        spec.input(self.input_name_output_folder).condition(ConditionType.NONE)  # Optional for overriding.\n",
    "        spec.output(self.output_name_result).condition(ConditionType.NONE)  # Not forcing a downstream receiver.\n",
    "\n",
    "    @property\n",
    "    def transform(self):\n",
    "        return Compose([EnsureChannelFirst(channel_dim=\"no_channel\"), ScaleIntensity(), EnsureType()])\n",
    "\n",
    "    def compute(self, op_input, op_output, context):\n",
    "        import json\n",
    "\n",
    "        import torch\n",
    "\n",
    "        img = op_input.receive(self.input_name_image).asnumpy()  # (64, 64), uint8. Input validation can be added.\n",
    "        image_tensor = self.transform(img)  # (1, 64, 64), torch.float64\n",
    "        image_tensor = image_tensor[None].float()  # (1, 1, 64, 64), torch.float32\n",
    "\n",
    "        device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
    "        image_tensor = image_tensor.to(device)\n",
    "\n",
    "        with torch.no_grad():\n",
    "            outputs = self.model(image_tensor)\n",
    "\n",
    "        _, output_classes = outputs.max(dim=1)\n",
    "\n",
    "        result = MEDNIST_CLASSES[output_classes[0]]  # get the class name\n",
    "        print(result)\n",
    "        op_output.emit(result, self.output_name_result)\n",
    "\n",
    "        # Get output folder, with value in optional input port overriding the obj attribute\n",
    "        output_folder_on_compute = op_input.receive(self.input_name_output_folder) or self.output_folder\n",
    "        Path.mkdir(output_folder_on_compute, parents=True, exist_ok=True)  # Let exception bubble up if raised.\n",
    "        output_path = output_folder_on_compute / \"output.json\"\n",
    "        with open(output_path, \"w\") as fp:\n",
    "            json.dump(result, fp)\n",
    "\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Creating Application class\n",
    "\n",
    "Our application class would look like below.\n",
    "\n",
    "It defines `App` class inheriting `Application` class.\n",
    "\n",
    "`LoadPILOperator` is connected to `MedNISTClassifierOperator` by using `self.add_flow()` in `compose()` method of `App`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "class App(Application):\n",
    "    \"\"\"Application class for the MedNIST classifier.\"\"\"\n",
    "\n",
    "    def compose(self):\n",
    "        app_context = Application.init_app_context({})  # Do not pass argv in Jupyter Notebook\n",
    "        app_input_path = Path(app_context.input_path)\n",
    "        app_output_path = Path(app_context.output_path)\n",
    "        model_path = Path(app_context.model_path)\n",
    "        load_pil_op = LoadPILOperator(self, CountCondition(self, 1), input_folder=app_input_path, name=\"pil_loader_op\")\n",
    "        classifier_op = MedNISTClassifierOperator(\n",
    "            self, app_context=app_context, output_folder=app_output_path, model_path=model_path, name=\"classifier_op\"\n",
    "        )\n",
    "\n",
    "        my_model_info = ModelInfo(\"MONAI WG Trainer\", \"MEDNIST Classifier\", \"0.1\", \"xyz\")\n",
    "        my_equipment = EquipmentInfo(manufacturer=\"MOANI Deploy App SDK\", manufacturer_model=\"DICOM SR Writer\")\n",
    "        my_special_tags = {\"SeriesDescription\": \"Not for clinical use. The result is for research use only.\"}\n",
    "        dicom_sr_operator = DICOMTextSRWriterOperator(\n",
    "            self,\n",
    "            copy_tags=False,\n",
    "            model_info=my_model_info,\n",
    "            equipment_info=my_equipment,\n",
    "            custom_tags=my_special_tags,\n",
    "            output_folder=app_output_path,\n",
    "        )\n",
    "\n",
    "        self.add_flow(load_pil_op, classifier_op, {(\"image\", \"image\")})\n",
    "        self.add_flow(classifier_op, dicom_sr_operator, {(\"result_text\", \"text\")})\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Executing app locally"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can execute the app in the Jupyter notebook. Before doing so, we also need to clean the output folder which was created by running the packaged containerizd app in the previous cell."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[info] [fragment.cpp:705] Loading extensions from configs...\n",
      "[2025-04-22 10:01:06,211] [INFO] (root) - Parsed args: Namespace(log_level=None, input=None, output=None, model=None, workdir=None, triton_server_netloc=None, argv=[])\n",
      "[2025-04-22 10:01:06,224] [INFO] (root) - AppContext object: AppContext(input_path=input, output_path=output, model_path=models, workdir=), triton_server_netloc=\n",
      "[info] [gxf_executor.cpp:265] Creating context\n",
      "[info] [gxf_executor.cpp:2396] Activating Graph...\n",
      "[info] [gxf_executor.cpp:2426] Running Graph...\n",
      "[info] [gxf_executor.cpp:2428] Waiting for completion...\n",
      "[info] [greedy_scheduler.cpp:191] Scheduling 3 entities\n",
      "/home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages/monai/data/meta_tensor.py:116: UserWarning: The given NumPy array is not writable, and PyTorch does not support non-writable tensors. This means writing to this tensor will result in undefined behavior. You may want to copy the array to protect its data or make it writable before converting it to a tensor. This type of warning will be suppressed for the rest of this program. (Triggered internally at /pytorch/torch/csrc/utils/tensor_numpy.cpp:203.)\n",
      "  return torch.as_tensor(x, *args, **_kwargs).as_subclass(cls)\n",
      "[2025-04-22 10:01:07,561] [WARNING] (pydicom) - 'Dataset.is_implicit_VR' will be removed in v4.0, set the Transfer Syntax UID or use the 'implicit_vr' argument with Dataset.save_as() or dcmwrite() instead\n",
      "[2025-04-22 10:01:07,562] [WARNING] (pydicom) - 'Dataset.is_little_endian' will be removed in v4.0, set the Transfer Syntax UID or use the 'little_endian' argument with Dataset.save_as() or dcmwrite() instead\n",
      "[2025-04-22 10:01:07,565] [WARNING] (pydicom) - Invalid value for VR UI: 'xyz'. Please see <https://dicom.nema.org/medical/dicom/current/output/html/part05.html#table_6.2-1> for allowed values for each VR.\n",
      "/home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages/pydicom/valuerep.py:440: UserWarning: Invalid value for VR UI: 'xyz'. Please see <https://dicom.nema.org/medical/dicom/current/output/html/part05.html#table_6.2-1> for allowed values for each VR.\n",
      "  warn_and_log(msg)\n",
      "[2025-04-22 10:01:07,575] [WARNING] (pydicom) - 'write_like_original' is deprecated and will be removed in v4.0, please use 'enforce_file_format' instead\n",
      "[2025-04-22 10:01:07,581] [INFO] (root) - Finished writing DICOM instance to file output/1.2.826.0.1.3680043.8.498.59762034317112105131069375575619402726.dcm\n",
      "[2025-04-22 10:01:07,585] [INFO] (monai.deploy.operators.dicom_text_sr_writer_operator.DICOMTextSRWriterOperator) - DICOM SOP instance saved in output/1.2.826.0.1.3680043.8.498.59762034317112105131069375575619402726.dcm\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "AbdomenCT\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[info] [greedy_scheduler.cpp:372] Scheduler stopped: Some entities are waiting for execution, but there are no periodic or async entities to get out of the deadlock.\n",
      "[info] [greedy_scheduler.cpp:401] Scheduler finished.\n",
      "[info] [gxf_executor.cpp:2431] Deactivating Graph...\n",
      "[info] [gxf_executor.cpp:2439] Graph execution finished.\n",
      "[info] [gxf_executor.cpp:295] Destroying context\n"
     ]
    }
   ],
   "source": [
    "!rm -rf $HOLOSCAN_OUTPUT_PATH\n",
    "app = App().run()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\"AbdomenCT\""
     ]
    }
   ],
   "source": [
    "!cat $HOLOSCAN_OUTPUT_PATH/output.json"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "tags": []
   },
   "source": [
    "Once the application is verified inside Jupyter notebook, we can write the whole application as a file(`mednist_classifier_monaideploy.py`) by concatenating code above, then add the following lines:\n",
    "\n",
    "```python\n",
    "if __name__ == \"__main__\":\n",
    "    App().run()\n",
    "```\n",
    "\n",
    "The above lines are needed to execute the application code by using `python` interpreter."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create an application folder\n",
    "!mkdir -p mednist_app && rm -rf mednist_app/*"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Writing mednist_app/mednist_classifier_monaideploy.py\n"
     ]
    }
   ],
   "source": [
    "%%writefile mednist_app/mednist_classifier_monaideploy.py\n",
    "\n",
    "# Copyright 2021-2023 MONAI Consortium\n",
    "# Licensed under the Apache License, Version 2.0 (the \"License\");\n",
    "# you may not use this file except in compliance with the License.\n",
    "# You may obtain a copy of the License at\n",
    "#     http://www.apache.org/licenses/LICENSE-2.0\n",
    "# Unless required by applicable law or agreed to in writing, software\n",
    "# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
    "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
    "# See the License for the specific language governing permissions and\n",
    "# limitations under the License.\n",
    "\n",
    "import logging\n",
    "import os\n",
    "from pathlib import Path\n",
    "from typing import Optional\n",
    "\n",
    "import torch\n",
    "\n",
    "from monai.deploy.conditions import CountCondition\n",
    "from monai.deploy.core import AppContext, Application, ConditionType, Fragment, Image, Operator, OperatorSpec\n",
    "from monai.deploy.operators.dicom_text_sr_writer_operator import DICOMTextSRWriterOperator, EquipmentInfo, ModelInfo\n",
    "from monai.transforms import EnsureChannelFirst, Compose, EnsureType, ScaleIntensity\n",
    "\n",
    "MEDNIST_CLASSES = [\"AbdomenCT\", \"BreastMRI\", \"CXR\", \"ChestCT\", \"Hand\", \"HeadCT\"]\n",
    "\n",
    "\n",
    "# @md.env(pip_packages=[\"pillow\"])\n",
    "class LoadPILOperator(Operator):\n",
    "    \"\"\"Load image from the given input (DataPath) and set numpy array to the output (Image).\"\"\"\n",
    "\n",
    "    DEFAULT_INPUT_FOLDER = Path.cwd() / \"input\"\n",
    "    DEFAULT_OUTPUT_NAME = \"image\"\n",
    "\n",
    "    # For now, need to have the input folder as an instance attribute, set on init.\n",
    "    # If dynamically changing the input folder, per compute, then use a (optional) input port to convey the\n",
    "    # value of the input folder, which is then emitted by a upstream operator.\n",
    "    def __init__(\n",
    "        self,\n",
    "        fragment: Fragment,\n",
    "        *args,\n",
    "        input_folder: Path = DEFAULT_INPUT_FOLDER,\n",
    "        output_name: str = DEFAULT_OUTPUT_NAME,\n",
    "        **kwargs,\n",
    "    ):\n",
    "        \"\"\"Creates an loader object with the input folder and the output port name overrides as needed.\n",
    "\n",
    "        Args:\n",
    "            fragment (Fragment): An instance of the Application class which is derived from Fragment.\n",
    "            input_folder (Path): Folder from which to load input file(s).\n",
    "                                 Defaults to `input` in the current working directory.\n",
    "            output_name (str): Name of the output port, which is an image object. Defaults to `image`.\n",
    "        \"\"\"\n",
    "\n",
    "        self._logger = logging.getLogger(\"{}.{}\".format(__name__, type(self).__name__))\n",
    "        self.input_path = input_folder\n",
    "        self.index = 0\n",
    "        self.output_name_image = (\n",
    "            output_name.strip() if output_name and len(output_name.strip()) > 0 else LoadPILOperator.DEFAULT_OUTPUT_NAME\n",
    "        )\n",
    "\n",
    "        super().__init__(fragment, *args, **kwargs)\n",
    "\n",
    "    def setup(self, spec: OperatorSpec):\n",
    "        \"\"\"Set up the named input and output port(s)\"\"\"\n",
    "        spec.output(self.output_name_image)\n",
    "\n",
    "    def compute(self, op_input, op_output, context):\n",
    "        import numpy as np\n",
    "        from PIL import Image as PILImage\n",
    "\n",
    "        # Input path is stored in the object attribute, but could change to use a named port if need be.\n",
    "        input_path = self.input_path\n",
    "        if input_path.is_dir():\n",
    "            input_path = next(self.input_path.glob(\"*.*\"))  # take the first file\n",
    "\n",
    "        image = PILImage.open(input_path)\n",
    "        image = image.convert(\"L\")  # convert to greyscale image\n",
    "        image_arr = np.asarray(image)\n",
    "\n",
    "        output_image = Image(image_arr)  # create Image domain object with a numpy array\n",
    "        op_output.emit(output_image, self.output_name_image)  # cannot omit the name even if single output.\n",
    "\n",
    "\n",
    "# @md.env(pip_packages=[\"monai\"])\n",
    "class MedNISTClassifierOperator(Operator):\n",
    "    \"\"\"Classifies the given image and returns the class name.\n",
    "\n",
    "    Named inputs:\n",
    "        image: Image object for which to generate the classification.\n",
    "        output_folder: Optional, the path to save the results JSON file, overridingthe the one set on __init__\n",
    "\n",
    "    Named output:\n",
    "        result_text: The classification results in text.\n",
    "    \"\"\"\n",
    "\n",
    "    DEFAULT_OUTPUT_FOLDER = Path.cwd() / \"classification_results\"\n",
    "    # For testing the app directly, the model should be at the following path.\n",
    "    MODEL_LOCAL_PATH = Path(os.environ.get(\"HOLOSCAN_MODEL_PATH\", Path.cwd() / \"model/model.ts\"))\n",
    "\n",
    "    def __init__(\n",
    "        self,\n",
    "        fragment: Fragment,\n",
    "        *args,\n",
    "        app_context: AppContext,\n",
    "        model_name: Optional[str] = \"\",\n",
    "        model_path: Path = MODEL_LOCAL_PATH,\n",
    "        output_folder: Path = DEFAULT_OUTPUT_FOLDER,\n",
    "        **kwargs,\n",
    "    ):\n",
    "        \"\"\"Creates an instance with the reference back to the containing application/fragment.\n",
    "\n",
    "        fragment (Fragment): An instance of the Application class which is derived from Fragment.\n",
    "        model_name (str, optional): Name of the model. Default to \"\" for single model app.\n",
    "        model_path (Path): Path to the model file. Defaults to model/models.ts of current working dir.\n",
    "        output_folder (Path, optional): output folder for saving the classification results JSON file.\n",
    "        \"\"\"\n",
    "\n",
    "        # the names used for the model inference input and output\n",
    "        self._input_dataset_key = \"image\"\n",
    "        self._pred_dataset_key = \"pred\"\n",
    "\n",
    "        # The names used for the operator input and output\n",
    "        self.input_name_image = \"image\"\n",
    "        self.output_name_result = \"result_text\"\n",
    "\n",
    "        # The name of the optional input port for passing data to override the output folder path.\n",
    "        self.input_name_output_folder = \"output_folder\"\n",
    "\n",
    "        # The output folder set on the object can be overridden at each compute by data in the optional named input\n",
    "        self.output_folder = output_folder\n",
    "\n",
    "        # Need the name when there are multiple models loaded\n",
    "        self._model_name = model_name.strip() if isinstance(model_name, str) else \"\"\n",
    "        # Need the path to load the models when they are not loaded in the execution context\n",
    "        self.model_path = model_path\n",
    "        self.app_context = app_context\n",
    "        self.model = self._get_model(self.app_context, self.model_path, self._model_name)\n",
    "\n",
    "        # This needs to be at the end of the constructor.\n",
    "        super().__init__(fragment, *args, **kwargs)\n",
    "\n",
    "    def _get_model(self, app_context: AppContext, model_path: Path, model_name: str):\n",
    "        \"\"\"Load the model with the given name from context or model path\n",
    "\n",
    "        Args:\n",
    "            app_context (AppContext): The application context object holding the model(s)\n",
    "            model_path (Path): The path to the model file, as a backup to load model directly\n",
    "            model_name (str): The name of the model, when multiples are loaded in the context\n",
    "        \"\"\"\n",
    "\n",
    "        if app_context.models:\n",
    "            # `app_context.models.get(model_name)` returns a model instance if exists.\n",
    "            # If model_name is not specified and only one model exists, it returns that model.\n",
    "            model = app_context.models.get(model_name)\n",
    "        else:\n",
    "            model = torch.jit.load(\n",
    "                MedNISTClassifierOperator.MODEL_LOCAL_PATH,\n",
    "                map_location=torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\"),\n",
    "            )\n",
    "\n",
    "        return model\n",
    "\n",
    "    def setup(self, spec: OperatorSpec):\n",
    "        \"\"\"Set up the operator named input and named output, both are in-memory objects.\"\"\"\n",
    "\n",
    "        spec.input(self.input_name_image)\n",
    "        spec.input(self.input_name_output_folder).condition(ConditionType.NONE)  # Optional for overriding.\n",
    "        spec.output(self.output_name_result).condition(ConditionType.NONE)  # Not forcing a downstream receiver.\n",
    "\n",
    "    @property\n",
    "    def transform(self):\n",
    "        return Compose([EnsureChannelFirst(channel_dim=\"no_channel\"), ScaleIntensity(), EnsureType()])\n",
    "\n",
    "    def compute(self, op_input, op_output, context):\n",
    "        import json\n",
    "\n",
    "        import torch\n",
    "\n",
    "        img = op_input.receive(self.input_name_image).asnumpy()  # (64, 64), uint8. Input validation can be added.\n",
    "        image_tensor = self.transform(img)  # (1, 64, 64), torch.float64\n",
    "        image_tensor = image_tensor[None].float()  # (1, 1, 64, 64), torch.float32\n",
    "\n",
    "        device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
    "        image_tensor = image_tensor.to(device)\n",
    "\n",
    "        with torch.no_grad():\n",
    "            outputs = self.model(image_tensor)\n",
    "\n",
    "        _, output_classes = outputs.max(dim=1)\n",
    "\n",
    "        result = MEDNIST_CLASSES[output_classes[0]]  # get the class name\n",
    "        print(result)\n",
    "        op_output.emit(result, self.output_name_result)\n",
    "\n",
    "        # Get output folder, with value in optional input port overriding the obj attribute\n",
    "        output_folder_on_compute = op_input.receive(self.input_name_output_folder) or self.output_folder\n",
    "        Path.mkdir(output_folder_on_compute, parents=True, exist_ok=True)  # Let exception bubble up if raised.\n",
    "        output_path = output_folder_on_compute / \"output.json\"\n",
    "        with open(output_path, \"w\") as fp:\n",
    "            json.dump(result, fp)\n",
    "\n",
    "\n",
    "# @md.resource(cpu=1, gpu=1, memory=\"1Gi\")\n",
    "class App(Application):\n",
    "    \"\"\"Application class for the MedNIST classifier.\"\"\"\n",
    "\n",
    "    def compose(self):\n",
    "        # Use Commandline options over environment variables to init context.\n",
    "        app_context = Application.init_app_context(self.argv)\n",
    "        app_input_path = Path(app_context.input_path)\n",
    "        app_output_path = Path(app_context.output_path)\n",
    "        model_path = Path(app_context.model_path)\n",
    "        load_pil_op = LoadPILOperator(self, CountCondition(self, 1), input_folder=app_input_path, name=\"pil_loader_op\")\n",
    "        classifier_op = MedNISTClassifierOperator(\n",
    "            self, app_context=app_context, output_folder=app_output_path, model_path=model_path, name=\"classifier_op\"\n",
    "        )\n",
    "\n",
    "        my_model_info = ModelInfo(\"MONAI WG Trainer\", \"MEDNIST Classifier\", \"0.1\", \"xyz\")\n",
    "        my_equipment = EquipmentInfo(manufacturer=\"MOANI Deploy App SDK\", manufacturer_model=\"DICOM SR Writer\")\n",
    "        my_special_tags = {\"SeriesDescription\": \"Not for clinical use. The result is for research use only.\"}\n",
    "        dicom_sr_operator = DICOMTextSRWriterOperator(\n",
    "            self,\n",
    "            copy_tags=False,\n",
    "            model_info=my_model_info,\n",
    "            equipment_info=my_equipment,\n",
    "            custom_tags=my_special_tags,\n",
    "            output_folder=app_output_path,\n",
    "        )\n",
    "\n",
    "        self.add_flow(load_pil_op, classifier_op, {(\"image\", \"image\")})\n",
    "        self.add_flow(classifier_op, dicom_sr_operator, {(\"result_text\", \"text\")})\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    App().run()\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This time, let's execute the app on the command line.\n",
    "\n",
    ":::{note}\n",
    "Since the environment variables have been set and contain the correct paths, it is not necessary to provide the command line options on running the application, though the following demonstrates the use of the options.\n",
    ":::"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[\u001b[32minfo\u001b[m] [fragment.cpp:705] Loading extensions from configs...\n",
      "[2025-04-22 10:01:12,273] [INFO] (root) - Parsed args: Namespace(log_level='DEBUG', input=PosixPath('/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/input'), output=PosixPath('/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/output'), model=PosixPath('/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/models'), workdir=None, triton_server_netloc=None, argv=['mednist_app/mednist_classifier_monaideploy.py', '-i', 'input', '-o', 'output', '-m', 'models', '-l', 'DEBUG'])\n",
      "[2025-04-22 10:01:12,278] [INFO] (root) - AppContext object: AppContext(input_path=/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/input, output_path=/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/output, model_path=/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/models, workdir=), triton_server_netloc=\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:265] Creating context\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:2396] Activating Graph...\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:2426] Running Graph...\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:2428] Waiting for completion...\n",
      "[\u001b[32minfo\u001b[m] [greedy_scheduler.cpp:191] Scheduling 3 entities\n",
      "/home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages/monai/data/meta_tensor.py:116: UserWarning: The given NumPy array is not writable, and PyTorch does not support non-writable tensors. This means writing to this tensor will result in undefined behavior. You may want to copy the array to protect its data or make it writable before converting it to a tensor. This type of warning will be suppressed for the rest of this program. (Triggered internally at /pytorch/torch/csrc/utils/tensor_numpy.cpp:203.)\n",
      "  return torch.as_tensor(x, *args, **_kwargs).as_subclass(cls)\n",
      "AbdomenCT\n",
      "[2025-04-22 10:01:13,572] [DEBUG] (monai.deploy.operators.dicom_text_sr_writer_operator.DICOMTextSRWriterOperator) - Writing DICOM object...\n",
      "\n",
      "[2025-04-22 10:01:13,572] [DEBUG] (root) - Writing DICOM common modules...\n",
      "[2025-04-22 10:01:13,573] [WARNING] (pydicom) - 'Dataset.is_implicit_VR' will be removed in v4.0, set the Transfer Syntax UID or use the 'implicit_vr' argument with Dataset.save_as() or dcmwrite() instead\n",
      "[2025-04-22 10:01:13,573] [WARNING] (pydicom) - 'Dataset.is_little_endian' will be removed in v4.0, set the Transfer Syntax UID or use the 'little_endian' argument with Dataset.save_as() or dcmwrite() instead\n",
      "[2025-04-22 10:01:13,574] [WARNING] (pydicom) - Invalid value for VR UI: 'xyz'. Please see <https://dicom.nema.org/medical/dicom/current/output/html/part05.html#table_6.2-1> for allowed values for each VR.\n",
      "/home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages/pydicom/valuerep.py:440: UserWarning: Invalid value for VR UI: 'xyz'. Please see <https://dicom.nema.org/medical/dicom/current/output/html/part05.html#table_6.2-1> for allowed values for each VR.\n",
      "  warn_and_log(msg)\n",
      "[2025-04-22 10:01:13,576] [DEBUG] (root) - DICOM common modules written:\n",
      "Dataset.file_meta -------------------------------\n",
      "(0002,0000) File Meta Information Group Length  UL: 198\n",
      "(0002,0001) File Meta Information Version       OB: b'01'\n",
      "(0002,0002) Media Storage SOP Class UID         UI: Basic Text SR Storage\n",
      "(0002,0003) Media Storage SOP Instance UID      UI: 1.2.826.0.1.3680043.8.498.41171981535561245877202758927925418229\n",
      "(0002,0010) Transfer Syntax UID                 UI: Implicit VR Little Endian\n",
      "(0002,0012) Implementation Class UID            UI: 1.2.40.0.13.1.1.1\n",
      "(0002,0013) Implementation Version Name         SH: '0.5.1+37.g96f7e'\n",
      "-------------------------------------------------\n",
      "(0008,0005) Specific Character Set              CS: 'ISO_IR 100'\n",
      "(0008,0012) Instance Creation Date              DA: '20250422'\n",
      "(0008,0013) Instance Creation Time              TM: '100113'\n",
      "(0008,0016) SOP Class UID                       UI: Basic Text SR Storage\n",
      "(0008,0018) SOP Instance UID                    UI: 1.2.826.0.1.3680043.8.498.41171981535561245877202758927925418229\n",
      "(0008,0020) Study Date                          DA: '20250422'\n",
      "(0008,0021) Series Date                         DA: '20250422'\n",
      "(0008,0023) Content Date                        DA: '20250422'\n",
      "(0008,002A) Acquisition DateTime                DT: '20250422100113'\n",
      "(0008,0030) Study Time                          TM: '100113'\n",
      "(0008,0031) Series Time                         TM: '100113'\n",
      "(0008,0033) Content Time                        TM: '100113'\n",
      "(0008,0050) Accession Number                    SH: ''\n",
      "(0008,0060) Modality                            CS: 'SR'\n",
      "(0008,0070) Manufacturer                        LO: 'MOANI Deploy App SDK'\n",
      "(0008,0090) Referring Physician's Name          PN: ''\n",
      "(0008,0201) Timezone Offset From UTC            SH: '-0700'\n",
      "(0008,1030) Study Description                   LO: 'AI results.'\n",
      "(0008,103E) Series Description                  LO: 'CAUTION: Not for Diagnostic Use, for research use only.'\n",
      "(0008,1090) Manufacturer's Model Name           LO: 'DICOM SR Writer'\n",
      "(0010,0010) Patient's Name                      PN: ''\n",
      "(0010,0020) Patient ID                          LO: ''\n",
      "(0010,0021) Issuer of Patient ID                LO: ''\n",
      "(0010,0030) Patient's Birth Date                DA: ''\n",
      "(0010,0040) Patient's Sex                       CS: ''\n",
      "(0018,0015) Body Part Examined                  CS: ''\n",
      "(0018,1020) Software Versions                   LO: '0.5.1+37.g96f7e'\n",
      "(0018,A001)  Contributing Equipment Sequence  1 item(s) ---- \n",
      "   (0008,0070) Manufacturer                        LO: 'MONAI WG Trainer'\n",
      "   (0008,1090) Manufacturer's Model Name           LO: 'MEDNIST Classifier'\n",
      "   (0018,1002) Device UID                          UI: xyz\n",
      "   (0018,1020) Software Versions                   LO: '0.1'\n",
      "   (0040,A170)  Purpose of Reference Code Sequence  1 item(s) ---- \n",
      "      (0008,0100) Code Value                          SH: 'Newcode1'\n",
      "      (0008,0102) Coding Scheme Designator            SH: '99IHE'\n",
      "      (0008,0104) Code Meaning                        LO: '\"Processing Algorithm'\n",
      "      ---------\n",
      "   ---------\n",
      "(0020,000D) Study Instance UID                  UI: 1.2.826.0.1.3680043.8.498.21427650624285250793329047854027764031\n",
      "(0020,000E) Series Instance UID                 UI: 1.2.826.0.1.3680043.8.498.53141607669515853472048821908030378483\n",
      "(0020,0010) Study ID                            SH: '1'\n",
      "(0020,0011) Series Number                       IS: '1679'\n",
      "(0020,0013) Instance Number                     IS: '1'\n",
      "(0040,1001) Requested Procedure ID              SH: ''\n",
      "[2025-04-22 10:01:13,577] [DEBUG] (root) - DICOM dataset to be written:Dataset.file_meta -------------------------------\n",
      "(0002,0000) File Meta Information Group Length  UL: 198\n",
      "(0002,0001) File Meta Information Version       OB: b'01'\n",
      "(0002,0002) Media Storage SOP Class UID         UI: Basic Text SR Storage\n",
      "(0002,0003) Media Storage SOP Instance UID      UI: 1.2.826.0.1.3680043.8.498.41171981535561245877202758927925418229\n",
      "(0002,0010) Transfer Syntax UID                 UI: Implicit VR Little Endian\n",
      "(0002,0012) Implementation Class UID            UI: 1.2.40.0.13.1.1.1\n",
      "(0002,0013) Implementation Version Name         SH: '0.5.1+37.g96f7e'\n",
      "-------------------------------------------------\n",
      "(0008,0005) Specific Character Set              CS: 'ISO_IR 100'\n",
      "(0008,0012) Instance Creation Date              DA: '20250422'\n",
      "(0008,0013) Instance Creation Time              TM: '100113'\n",
      "(0008,0016) SOP Class UID                       UI: Basic Text SR Storage\n",
      "(0008,0018) SOP Instance UID                    UI: 1.2.826.0.1.3680043.8.498.41171981535561245877202758927925418229\n",
      "(0008,0020) Study Date                          DA: '20250422'\n",
      "(0008,0021) Series Date                         DA: '20250422'\n",
      "(0008,0023) Content Date                        DA: '20250422'\n",
      "(0008,002A) Acquisition DateTime                DT: '20250422100113'\n",
      "(0008,0030) Study Time                          TM: '100113'\n",
      "(0008,0031) Series Time                         TM: '100113'\n",
      "(0008,0033) Content Time                        TM: '100113'\n",
      "(0008,0050) Accession Number                    SH: ''\n",
      "(0008,0060) Modality                            CS: 'SR'\n",
      "(0008,0070) Manufacturer                        LO: 'MOANI Deploy App SDK'\n",
      "(0008,0090) Referring Physician's Name          PN: ''\n",
      "(0008,0201) Timezone Offset From UTC            SH: '-0700'\n",
      "(0008,1030) Study Description                   LO: 'AI results.'\n",
      "(0008,103E) Series Description                  LO: 'Not for clinical use. The result is for research use only.'\n",
      "(0008,1090) Manufacturer's Model Name           LO: 'DICOM SR Writer'\n",
      "(0010,0010) Patient's Name                      PN: ''\n",
      "(0010,0020) Patient ID                          LO: ''\n",
      "(0010,0021) Issuer of Patient ID                LO: ''\n",
      "(0010,0030) Patient's Birth Date                DA: ''\n",
      "(0010,0040) Patient's Sex                       CS: ''\n",
      "(0018,0015) Body Part Examined                  CS: ''\n",
      "(0018,1020) Software Versions                   LO: '0.5.1+37.g96f7e'\n",
      "(0018,A001)  Contributing Equipment Sequence  1 item(s) ---- \n",
      "   (0008,0070) Manufacturer                        LO: 'MONAI WG Trainer'\n",
      "   (0008,1090) Manufacturer's Model Name           LO: 'MEDNIST Classifier'\n",
      "   (0018,1002) Device UID                          UI: xyz\n",
      "   (0018,1020) Software Versions                   LO: '0.1'\n",
      "   (0040,A170)  Purpose of Reference Code Sequence  1 item(s) ---- \n",
      "      (0008,0100) Code Value                          SH: 'Newcode1'\n",
      "      (0008,0102) Coding Scheme Designator            SH: '99IHE'\n",
      "      (0008,0104) Code Meaning                        LO: '\"Processing Algorithm'\n",
      "      ---------\n",
      "   ---------\n",
      "(0020,000D) Study Instance UID                  UI: 1.2.826.0.1.3680043.8.498.21427650624285250793329047854027764031\n",
      "(0020,000E) Series Instance UID                 UI: 1.2.826.0.1.3680043.8.498.53141607669515853472048821908030378483\n",
      "(0020,0010) Study ID                            SH: '1'\n",
      "(0020,0011) Series Number                       IS: '1679'\n",
      "(0020,0013) Instance Number                     IS: '1'\n",
      "(0040,1001) Requested Procedure ID              SH: ''\n",
      "(0040,A040) Value Type                          CS: 'CONTAINER'\n",
      "(0040,A043)  Concept Name Code Sequence  1 item(s) ---- \n",
      "   (0008,0100) Code Value                          SH: '18748-4'\n",
      "   (0008,0102) Coding Scheme Designator            SH: 'LN'\n",
      "   (0008,0104) Code Meaning                        LO: 'Diagnostic Imaging Report'\n",
      "   ---------\n",
      "(0040,A050) Continuity Of Content               CS: 'SEPARATE'\n",
      "(0040,A493) Verification Flag                   CS: 'UNVERIFIED'\n",
      "(0040,A730)  Content Sequence  1 item(s) ---- \n",
      "   (0040,A010) Relationship Type                   CS: 'CONTAINS'\n",
      "   (0040,A040) Value Type                          CS: 'TEXT'\n",
      "   (0040,A043)  Concept Name Code Sequence  1 item(s) ---- \n",
      "      (0008,0100) Code Value                          SH: '111412'\n",
      "      (0008,0102) Coding Scheme Designator            SH: 'DCM'\n",
      "      (0008,0104) Code Meaning                        LO: 'Narrative Summary'\n",
      "      ---------\n",
      "   (0040,A160) Text Value                          UT: 'AbdomenCT'\n",
      "   ---------\n",
      "[2025-04-22 10:01:13,577] [WARNING] (pydicom) - 'write_like_original' is deprecated and will be removed in v4.0, please use 'enforce_file_format' instead\n",
      "[2025-04-22 10:01:13,580] [INFO] (root) - Finished writing DICOM instance to file /home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/output/1.2.826.0.1.3680043.8.498.41171981535561245877202758927925418229.dcm\n",
      "[2025-04-22 10:01:13,581] [INFO] (monai.deploy.operators.dicom_text_sr_writer_operator.DICOMTextSRWriterOperator) - DICOM SOP instance saved in /home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/output/1.2.826.0.1.3680043.8.498.41171981535561245877202758927925418229.dcm\n",
      "[\u001b[32minfo\u001b[m] [greedy_scheduler.cpp:372] Scheduler stopped: Some entities are waiting for execution, but there are no periodic or async entities to get out of the deadlock.\n",
      "[\u001b[32minfo\u001b[m] [greedy_scheduler.cpp:401] Scheduler finished.\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:2431] Deactivating Graph...\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:2439] Graph execution finished.\n",
      "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:295] Destroying context\n"
     ]
    }
   ],
   "source": [
    "!python \"mednist_app/mednist_classifier_monaideploy.py\" -i {input_folder} -o {output_folder} -m {models_folder} -l DEBUG"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\"AbdomenCT\""
     ]
    }
   ],
   "source": [
    "!cat {output_folder}/output.json"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Additional file required for packaging the app (creating MAP Docker image)\n",
    "\n",
    "In this version of the App SDK, we need to write out the configuration yaml file as well as the package requirements file, in the application folder."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Writing mednist_app/app.yaml\n"
     ]
    }
   ],
   "source": [
    "%%writefile mednist_app/app.yaml\n",
    "%YAML 1.2\n",
    "---\n",
    "application:\n",
    "  title: MONAI Deploy App Package - MedNIST Classifier App\n",
    "  version: 1.0\n",
    "  inputFormats: [\"file\"]\n",
    "  outputFormats: [\"file\"]\n",
    "\n",
    "resources:\n",
    "  cpu: 1\n",
    "  gpu: 1\n",
    "  memory: 1Gi\n",
    "  gpuMemory: 1Gi"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Writing mednist_app/requirements.txt\n"
     ]
    }
   ],
   "source": [
    "%%writefile mednist_app/requirements.txt\n",
    "monai>=1.2.0\n",
    "Pillow>=8.4.0\n",
    "pydicom>=2.3.0\n",
    "highdicom>=0.18.2\n",
    "SimpleITK>=2.0.0\n",
    "setuptools>=59.5.0 # for pkg_resources\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "By now, we have built the application and prepared all necessary files for create the MONAI Application Package (MAP)."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
